Skip to content

feat(smime): add S/MIME support via companion-app architecture#11033

Open
christine-ciphermail wants to merge 2 commits into
thunderbird:mainfrom
christine-ciphermail:upstream-smime-pr
Open

feat(smime): add S/MIME support via companion-app architecture#11033
christine-ciphermail wants to merge 2 commits into
thunderbird:mainfrom
christine-ciphermail:upstream-smime-pr

Conversation

@christine-ciphermail
Copy link
Copy Markdown

Summary

Adds end-to-end S/MIME support to Thunderbird for Android via a companion-app architecture, mirroring the existing OpenPGP integration: key material and cryptographic operations live in a separate provider app, and Thunderbird talks to it over a stable AIDL API.

A reference provider implementation (CipherMail for Android) is the first consumer of this API, but any S/MIME provider can implement it.

What's included

New module plugins/smime-api/ — versioned AIDL + Parcelables + client helper. Pluggable across providers, mirrored in the CipherMail companion repo. Independent licensing under Apache-2.0.

Integration in legacy core / UI:

  • Account-level Sign/Encrypt toggles, with per-account defaults (account settings → S/MIME)
  • Compose: per-message Sign/Encrypt checkboxes; encrypt-implies-sign enforced; lock/sign icons in the recipient bar reflect provider's certificate availability check
  • Message view: separate "signed" / "encrypted" status icons and an "Open in provider" action for messages Thunderbird can't render inline
  • S/MIME state persisted separately from the legacy crypto-enabled flag so a message is never sent silently unencrypted when the provider is unavailable

Safety behaviour:

  • If the provider isn't installed or returns USER_INTERACTION_REQUIRED (e.g. provider keystore is locked), the compose flow pauses and surfaces the action to the user — the message is preserved, not lost
  • Drafts are offered a folder assignment when the composer is closed before a save completes

Documentation:

  • docs/adr/0009-smime-companion-app-architecture.md — architectural decision and rationale (why companion app vs. built-in)
  • docs/security/smime-companion-threat-model.md — threat model for the inter-process boundary
  • docs/developer/writing-smime-provider.md — guide for implementers of the API
  • docs/user-guide/setup/enabling-smime.md — end-user setup walkthrough

Why companion-app and not built-in?

Same reasons OpenPGP went this route in K-9 Mail / Thunderbird for Android: cryptographic key material, certificate stores, PIN unlock flows, and smartcard support are non-trivial concerns that benefit from living in a single, dedicated, hardened process. Thunderbird stays a mail client; providers stay providers. See ADR-0009 for the full discussion.

Test plan

  • Build cleanly on AGP 9 / Gradle 9 / JDK 21
  • Existing unit tests pass; new tests cover RecipientPresenter.onSmimeCertCheckResult branches
  • Manual: enable S/MIME on an account, sign-only, encrypt+sign, send/receive between two devices using CipherMail provider
  • Manual: uninstall provider, confirm error path (no silent unencrypted send, message preserved)
  • Manual: provider keystore locked → USER_INTERACTION_REQUIRED flow opens the provider's unlock UI, retry succeeds

Notes

  • No breaking changes to existing behaviour for users who don't enable S/MIME on an account
  • API module is versioned independently (plugins/smime-api/CHANGELOG.md) so providers and Thunderbird can evolve at their own pace

@github-actions
Copy link
Copy Markdown
Contributor

Missing report label. Set exactly one of: report: include, report: exclude OR report: highlight.

@christine-ciphermail
Copy link
Copy Markdown
Author

Please apply report: include

I can't seem to add a lablelmyself.

@christine-ciphermail
Copy link
Copy Markdown
Author

How do I add a label?

Adds support for delegating S/MIME crypto operations to a separate
companion app over an AIDL service, paralleling the existing OpenPGP /
OpenKeychain integration. The reference provider is CipherMail
(com.ciphermail.android); other providers can implement the same API.

API surface (new module `plugins/smime-api/smime-api/`)
  - ISmimeService AIDL — execute(Intent, ParcelFileDescriptor, int) +
    createOutputPipe(int) for streaming bulk MIME data.
  - SmimeApi helper (sync + async execution wrappers, pipe management).
  - SmimeServiceConnection (bind lifecycle helper).
  - Parcelables: SmimeError, SmimeSignatureResult, SmimeDecryptionResult,
    SmimeCertificateInfo. All carry PARCELABLE_VERSION = 1.
  - Actions: CHECK_PERMISSION, DECRYPT_VERIFY, SIGN_AND_ENCRYPT,
    GET_CERTIFICATES, IMPORT_CERTIFICATE.

Receive-side integration
  - SmimeCryptoHelper (parallels MessageCryptoHelper for OpenPGP):
    detects S/MIME parts, binds to the provider, dispatches DECRYPT_VERIFY,
    surfaces RESULT_CODE_USER_INTERACTION_REQUIRED via PendingIntent so
    the host can launch the provider's passphrase dialog.
  - MessageCryptoStructureDetector.isSmimePart and helpers — detect
    application/pkcs7-mime and PKCS#7 multipart/signed.
  - CryptoResultAnnotation: new S/MIME fields and createSmime* factories.
  - MessageCryptoDisplayStatus: S/MIME signature/encryption mappings to
    the existing display-status badges.
  - MessageLoaderHelper, MessageCryptoPresenter, MessageViewInfoExtractor
    wired through.

Send-side integration
  - SmimeMessageBuilder (parallels PgpMessageBuilder): binds to the
    provider on a background thread, calls SIGN_AND_ENCRYPT, returns the
    wrapped MIME message for SMTP transport. Drafts bypass crypto.
  - MessageCompose: S/MIME branch in createMessageBuilder(), checked
    before PGP.
  - RecipientPresenter.asyncUpdateSmimeCertStatus: calls
    GET_CERTIFICATES on recipient changes; drives the compose lock-icon
    state (green = all certs present, red = missing).

Per-account configuration
  - LegacyAccount / LegacyAccountDto: smimeProvider field +
    isSmimeProviderConfigured.
  - LegacyAccountStorageHandler + DefaultLegacyAccountDataMapper:
    persist smimeProvider.
  - AccountSettingsFragment: S/MIME PreferenceScreen + provider picker.
  - SmimeAppSelectDialog: enumerates installed providers via
    SmimeApi.SERVICE_INTENT and lets the user choose. Binding always
    uses setPackage(account.smimeProvider) to avoid intent-filter
    interception.

Manifest plumbing
  - app-k9mail and app-thunderbird AndroidManifest: <queries> for
    ISmimeService discovery on Android 11+.
  - legacy/ui/legacy AndroidManifest: register SmimeAppSelectDialog.

Cross-process passphrase handshake
  - When the provider's keystore is locked it returns
    RESULT_CODE_USER_INTERACTION_REQUIRED with an immutable PendingIntent
    for its passphrase activity. Thunderbird launches via
    startIntentSenderForResult and retries on RESULT_OK. No inline
    prompting, no IPC timeouts.

Send/receive UX and per-identity signing
  - User-controllable Sign / Encrypt toggles with per-account defaults;
    S/MIME-enabled is persisted separately and a message is never sent
    silently unencrypted.
  - Encrypt-implies-sign enforced (no encrypt without a signature).
  - Per-identity signing: EXTRA_FROM carries the composing account's
    address on SIGN_AND_ENCRYPT so the provider signs with the matching
    certificate.
  - Separate signed / encrypted status icons and an "open in provider"
    action to view any encrypted or signed message in CipherMail.
  - Provider-unavailable handling: never lose a message; gate composer
    close on save completion; offer Drafts-folder assignment.
  - Send failures surface in a dismissible dialog rather than a Toast.
  - Set the intent extras class loader before reading Parcelable extras.
  - Test: RecipientPresenter.onSmimeCertCheckResult result branches.
…rovider guide

Adds end-to-end documentation for the S/MIME companion integration:

Library docs (`plugins/smime-api/`)
  - README — client-side tutorial (bind, execute, result + PendingIntent
    flow) and action-by-action reference. Mirrors openpgp-api-lib/README.
  - CHANGELOG — Version 1 inventory of actions / extras / Parcelables.
  - LICENSE — Apache 2.0 (matches openpgp-api-lib).

mdbook docs
  - architecture/adr/0009 — Companion App + AIDL Service for S/MIME:
    decision record covering the three alternatives (in-process library,
    embedded crypto core, companion app) and why the companion-app model
    was chosen. Includes a Mermaid sequence diagram for the sign+encrypt
    + passphrase-unlock flow.
  - security/smime-companion-threat-model — STRIDE pass on the IPC trust
    boundary: provider discovery, binding, request/result tampering,
    PendingIntent hijacking, pipe DoS, cert-lookup honesty. Risks ranked,
    residual-risk notes for the two trade-offs inherent to the model.
  - user-guide/setup/enabling-smime — end-user walkthrough (install
    provider, set keystore passphrase, import certificate, enable
    S/MIME on the account, first send/receive). Includes a compose
    lock-icon state reference and a translator's inventory of the new
    string resources.
  - developer/writing-smime-provider — normative spec for implementing
    an alternative S/MIME provider: manifest declarations, AIDL contract,
    per-action behaviour and edge cases, the user-interaction handshake,
    EXTRA_API_VERSION negotiation, EXTRA_FROM for per-identity signing,
    security obligations (caller identity, no outbound network,
    trust-signal honesty), a testing checklist.
  - SUMMARY.md — all four new documents wired into the mdbook TOC.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants